#include <stdio.h>
#include <stdlib.h>
#include <SFML/Window.hpp>
#define _USE_MATH_DEFINES
#include <math.h>

#define RESOLUTION 512

#define TRUE 1
#define FALSE 0

#define TRACKBALLSIZE (0.8)          // trackball size in percentage of window
#define Z_SENSITIVITY 0.01           // used to scale translations in z
#define XY_SENSITIVITY 0.01          // used to scale translations in x and y
#define TARGET_FPS 30                // controls spin update rate
#define TIME_WINDOW 3                // number of frames motion is valid after release

float motionTime = .0f;
int lastPos[2] = {0,0};
int buttonDown[3] = {0,0};
int spin=FALSE;                    // are we spinning?
int xsize, ysize;                  // window size

// Storage space for the various transformations we'll need
float translation1[16], translation2[16], rotation[16], inc_rotation[16];

#if 0
GLfloat vertices[][3] = {{-1.0, -1.0, -1.0},
{1.0, -1.0, -1.0},
{1.0, 1.0, -1.0},
{-1.0, 1.0, -1.0},
{-1.0, -1.0, 1.0},
{1.0, -1.0, 1.0},
{1.0, 1.0, 1.0},
{-1.0, 1.0, 1.0}};
#else
GLfloat vertices[][3] = {{0.0, 0.0, 0.0},
{2.0, 0.0, 0.0},
{2.0, 2.0, 0.0},
{0.0, 2.0, 0.0},
{0.0, 0.0, 2.0},
{2.0, 0.0, 2.0},
{2.0, 2.0, 2.0},
{0.0, 2.0, 2.0}};
#endif

GLfloat colors[][3] = {{0.0, 0.0, 0.0},
{1.0, 0.0, 0.0},
{1.0, 1.0, 0.0},
{0.0, 1.0, 0.0},
{0.0, 0.0, 1.0},
{1.0, 0.0, 1.0},
{1.0, 1.0, 1.0},
{0.0, 1.0, 1.0}};

// draw a rectangle using the index of each vertex and color
void
	draw_polygon(int a, int b, int c, int d, int face)
{
	glBegin(GL_POLYGON);
	{
		glColor3fv(colors[a]);
		glVertex3fv(vertices[a]);
		glColor3fv(colors[b]);
		glVertex3fv(vertices[b]);
		glColor3fv(colors[c]);
		glVertex3fv(vertices[c]);
		glColor3fv(colors[d]);
		glVertex3fv(vertices[d]);
	}
	glEnd();
}

// draw a cube
void
	draw_color_cube(void)
{
	draw_polygon(1, 0, 3, 2, 0);
	draw_polygon(3, 7, 6, 2, 1);
	draw_polygon(7, 3, 0, 4, 2);
	draw_polygon(2, 6, 5, 1, 3);
	draw_polygon(4, 5, 6, 7, 4);
	draw_polygon(5, 4, 0, 1, 5);
}

void
	display()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	gluLookAt(0.0, 0.0, 10.0, 0.0, 0.0, 9.0, 0.0, 1.0, 0.0);
	glMultMatrixf(translation1);
	glMultMatrixf(rotation);
	glMultMatrixf(inc_rotation);
	draw_color_cube();
}

// update the modelview matrix with a new translation in the z direction
void
	update_z(int x1, int y1, int x2, int y2)
{
	float dz = y2 - y1;
	dz *= Z_SENSITIVITY;

	translation1[14] += dz;
}

// update the modelview matrix with a new translation in the x and/or y direction
void
	update_trans(int x1, int y1, int x2, int y2)
{
	float dx = x2 - x1;
	float dy = y2 - y1;

	dx *= XY_SENSITIVITY;
	dy *= XY_SENSITIVITY;

	translation1[12] += dx;
	translation1[13] += -dy;
}

// find the z coordinate corresponding to the mouse position 
float
	project_to_sphere(float r, float x, float y)
{
	float d, t, z;

	d = sqrt(x*x + y*y);
	t = r / M_SQRT2;
	if (d < t) {       // Inside sphere
		z = sqrt(r*r - d*d);
	}
	else {             // On hyperbola
		z = t*t / d;
	}

	return z;
}

// update the modelview matrix with a new rotation
void
	update_rotate(int x1, int y1, int x2, int y2)
{
	float axis[3], p1[3], p2[3];
	float phi, t;

	if (x1 == x2 && y1 == y2) { // if there's no movement, no rotation
		axis[0] = 1;
		axis[1] = 0;
		axis[2] = 0;
		phi = 0;
	}
	else {
		// first vector
		p1[0] = (2.0*x1)/(float)xsize - 1.0;
		p1[1] = 1.0 - (2.0*y1)/(float)ysize;
		p1[2] = project_to_sphere((float)TRACKBALLSIZE, p1[0], p1[1]);
		// second vector
		p2[0] = (2.0*x2)/(float)xsize - 1.0;
		p2[1] = 1.0 - (2.0*y2)/(float)ysize;
		p2[2] = project_to_sphere((float)TRACKBALLSIZE, p2[0], p2[1]);

		// the axis of rotation is given by the cross product of the first
		// and second vectors
		axis[0] = p1[1]*p2[2] - p1[2]*p2[1];
		axis[1] = p1[2]*p2[0] - p1[0]*p2[2];
		axis[2] = p1[0]*p2[1] - p1[1]*p2[0];
		t = axis[0]*axis[0] + axis[1]*axis[1] + axis[2]*axis[2];
		axis[0] /= t;
		axis[1] /= t;
		axis[2] /= t;

		// the amount of rotation is proportional to the magnitude of the
		// difference between the vectors
		t = sqrt((p1[0]-p2[0])*(p1[0]-p2[0]) + (p1[1]-p2[1])*(p1[1]-p2[1]) +
			(p1[2]-p2[2])*(p1[2]-p2[2]))/(2.0*TRACKBALLSIZE);

		if (t > 1.0) {
			t = 1.0;
		}
		if (t < -1.0) {
			t = -1.0;
		}
		phi = 360.0*asin(t)/M_PI;
	}

	//
	//
	// Put your code for a rotation of phi about axis[] here.
	//
	//
}

// function used to spin the object.
void
	SpinCube2(int t)
{
	if (spin) {  // if we're still spinning, increment the rotation
		//
		//  
		// Put your code for an incremental rotation here.  
		//  
		// 
	}
}

void
	reshape(int w, int h)
{
	glViewport(0, 0, w, h);
	xsize = w;
	ysize = h;
	
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(60, ((float)w)/h, 0.1f, 100.f);
}

void gfxinit()
{
	int i;
	float x=0, y=0, z=0;

	glEnable(GL_DEPTH_TEST);

	// initialize the projection stack
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(60.0, 1.0, 0.1, 100);

	// initialize the modelview stack
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	glGetFloatv(GL_MODELVIEW_MATRIX, (GLfloat *)&translation2);
	glGetFloatv(GL_MODELVIEW_MATRIX, (GLfloat *)&rotation);
	glGetFloatv(GL_MODELVIEW_MATRIX, (GLfloat *)&inc_rotation);
	for (i=0; i<8; i++) {
		x += vertices[i][0];
		y += vertices[i][1];
		z += vertices[i][2];
	}
	x /= 8;
	y /= 8;
	z /= 8;
	glTranslatef(-x, -y, -z);
	glGetFloatv(GL_MODELVIEW_MATRIX, (GLfloat *)&translation1);
	glLoadIdentity();
	gluLookAt(0.0, 0.0, 10.0, 0.0, 0.0, 9.0, 0.0, 1.0, 0.0);
	glMultMatrixf(translation1);

	glViewport(0, 0, RESOLUTION, RESOLUTION);
	xsize = RESOLUTION;
	ysize = RESOLUTION;
}


class GLBox
{
public:
	GLBox()
	{
		App = new sf::Window(sf::VideoMode(RESOLUTION, RESOLUTION, 32), "Trackball");

		gfxinit();

		while (App->IsOpened())
		{
			float targetFrameTime = 1.0f/(float)TARGET_FPS;
			float sleepTime = targetFrameTime - App->GetFrameTime();
			if(sleepTime > 0)
				sf::Sleep(sleepTime);

			App->SetActive();

			handleEvents();
			SpinCube2( 0 );
			display();

			App->Display();
		}
	}

private:
	sf::Window *App;
	sf::Clock motionClock;
	float timeSinceMotion;

	void handleEvents()
	{
		const sf::Input& Input = App->GetInput();
		bool shiftDown = Input.IsKeyDown(sf::Key::LShift) || Input.IsKeyDown(sf::Key::RShift);
		sf::Event Event;
		while (App->GetEvent(Event))
		{
			// Close window : exit
			if (Event.Type == sf::Event::Closed)
				App->Close();

			// Escape key : exit
			if ((Event.Type == sf::Event::KeyPressed) && (Event.Key.Code == sf::Key::Escape))
				App->Close();

			if (Event.Type == sf::Event::MouseButtonPressed)
			{
				lastPos[0] = Event.MouseButton.X;
				lastPos[1] = Event.MouseButton.Y;

				if(Event.MouseButton.Button == sf::Mouse::Left && !shiftDown)
				{
					buttonDown[0] = 1;
					spin = FALSE;
				}
				if(Event.MouseButton.Button == sf::Mouse::Right)
					buttonDown[1] = 1;
				if(Event.MouseButton.Button == sf::Mouse::Middle)
					buttonDown[2] = 1;
				if(Event.MouseButton.Button == sf::Mouse::Left && shiftDown)
					buttonDown[2] = 1;
			}

			if (Event.Type == sf::Event::MouseButtonReleased)
			{
				if(Event.MouseButton.Button == sf::Mouse::Left && !shiftDown)
					buttonDown[0] = 0;
				if(Event.MouseButton.Button == sf::Mouse::Right)
					buttonDown[1] = 0;
				if(Event.MouseButton.Button == sf::Mouse::Middle)
					buttonDown[2] = 0;
				if(Event.MouseButton.Button == sf::Mouse::Left && shiftDown)
					buttonDown[2] = 0;

				timeSinceMotion = motionClock.GetElapsedTime();
				float maxTime = 1.0f/(float)TARGET_FPS * TIME_WINDOW;
				if(timeSinceMotion < maxTime)
					spin = TRUE;
			}

			if (Event.Type == sf::Event::MouseMoved && (buttonDown[0] || buttonDown[1] || buttonDown[2]) )
			{
				int x = Event.MouseMove.X;
				int y = Event.MouseMove.Y;

				timeSinceMotion = motionClock.GetElapsedTime();
				motionClock.Reset();

				if(buttonDown[0])
					update_rotate(lastPos[0], lastPos[1], x, y);
				if(buttonDown[1])
					update_trans(lastPos[0], lastPos[1], x, y);
				if(buttonDown[2])
					update_z(lastPos[0], lastPos[1], x, y);

				lastPos[0] = x;
				lastPos[1] = y;
			}

			if (Event.Type == sf::Event::Resized)
			{
				reshape( Event.Size.Width, Event.Size.Height );
			}
		}
	}
};


int main(int argc, char **argv)
{
	GLBox prog;
	return EXIT_SUCCESS;
}